grammar Pxl;

options {
	output = AST;
}

tokens {
	INDENT;
	DEDENT;	
	NULL_STMT;
	ST_FUNCTION;
	ST_TRUE;
	ST_FALSE;
	ST_NONE;
	ST_IF;
	ST_FOR;
	ST_WHILE;
	ST_CLASS;
	ST_LAMBDA;
	ST_IMPORT;
	PARAMETERS;
	STMT_LIST;
	TEST_LIST;
    ARGS_LIST;
	CALL;
	LIST;
	MAP;
	SET;
	SUBSCRIPT;
	GET_ATTR;
	NOT_IN;
	NOT_IS;
    KEYWORDS;
    UNARY_PLUS;
    UNARY_MINUS;
    COMPARISON;
}

@lexer::header {
	package pxl.grammar.classes;
}

@parser::header {
	package pxl.grammar.classes;
	
	import pxl.types.util.*;
	import com.google.common.collect.Lists;
}

@lexer::members {
/** Handles context-sensitive lexing of implicit line joining such as
 *  the case where newline is ignored in cases like this:
 *  a = [3,
 *       4]
 */
protected int implicitLineJoiningLevel = 0;
protected int startPos=-1;
}

single_input
    : NEWLINE -> NULL_STMT
	| simple_stmt
	| compound_stmt NEWLINE!
	| EOF -> NULL_STMT
	;

file_input
    :   (NEWLINE! | stmt)*
	;

eval_input
    :   (NEWLINE)* testlist (NEWLINE)*
	;

funcdef
    :   'def' NAME parameters ':' suite ->
    		^(ST_FUNCTION NAME parameters suite)
	;

parameters
    :   '(' (varargslist)? ')' -> ^(PARAMETERS varargslist? )
	;

varargslist
    :   defparameter (options {greedy=true;}:COMMA! defparameter)*
        (COMMA! vararglist_trailer)? (COMMA!)?
    |   vararglist_trailer (COMMA!)?
    ;

vararglist_trailer
    : ( STAR n1=NAME -> ^(STAR $n1) )
        ((COMMA DOUBLESTAR n2=NAME -> ^(STAR $n1) ^(DOUBLESTAR $n2) ))?
    | DOUBLESTAR NAME -> ^(DOUBLESTAR NAME)
    ;

defparameter
    :   fpdef (ASSIGN^ test)?
    ;

fpdef
    :   NAME
	|   LPAREN! fplist RPAREN!
	;

fplist
    :   fpdef (options {greedy=true;}:COMMA fpdef)* (COMMA)?
	;


stmt: simple_stmt
	| compound_stmt
	;

simple_stmt
    	:   small_stmt (options {greedy=true;}:';' small_stmt)* (';')? NEWLINE
    		-> small_stmt+
	;

small_stmt: expr_stmt
	| del_stmt
	| pass_stmt
	| flow_stmt
	| import_stmt
	| global_stmt
	| exec_stmt
	| assert_stmt
	;

expr_stmt
	:	testlist
		(	augassign^ testlist
		|	(ASSIGN^ testlist)+
		)?
	;

augassign
    : PLUSEQUAL
	| MINUSEQUAL
	| STAREQUAL
	| SLASHEQUAL
	| PERCENTEQUAL
	| AMPEREQUAL
	| VBAREQUAL
	| CIRCUMFLEXEQUAL
	| LEFTSHIFTEQUAL
	| RIGHTSHIFTEQUAL
	| DOUBLESTAREQUAL
	| DOUBLESLASHEQUAL
	;

del_stmt: 'del' exprlist
	;

pass_stmt: 'pass'
	;

flow_stmt: break_stmt
	| continue_stmt
	| return_stmt
	| raise_stmt
	| yield_stmt
	;

break_stmt: 'break'
	;

continue_stmt: 'continue'
	;

return_stmt: 'return'^ (testlist)?
	;

yield_stmt: 'yield' testlist
	;

raise_stmt: 'raise' (test ('*' test (',' test)?)?)?
	;

import_stmt
	:	'import' dotted_as_name (COMMA dotted_as_name)* -> ^(ST_IMPORT dotted_as_name+)
	|	'from' dotted_name 'import'
     			('*' | import_as_name (COMMA import_as_name)*)
	;

import_as_name
    :   NAME ('as' NAME)?
	;

dotted_as_name: dotted_name ('as'^ NAME)?
	;

dotted_name 
@init {
	String name = null;
}
	: n1=NAME { name = $n1.text; } 
			('.' n2=NAME { name += '.' + $n2.text; } )*
	  -> { new CommonTree( new CommonToken(NAME, name) ) }
	;

global_stmt: 'global' NAME (',' NAME)* -> ^('global' NAME+)
	;

exec_stmt: 'exec' expr ('in' test (',' test)?)?
	;

assert_stmt: 'assert' test (',' test)? -> ^('assert' test test?)
	;


compound_stmt: if_stmt -> ^(ST_IF if_stmt)
	| while_stmt
	| for_stmt
	| try_stmt
	| funcdef
	| classdef
	;

if_stmt: 'if'! test ':'! suite 
		('elif'! test ':'! suite)* 
		('else'! ':'! suite)? 
	;

while_stmt: 'while' test ':' suite 
			('else' ':' suite)?
				-> ^(ST_WHILE test suite suite?) ;

for_stmt: 'for' exprlist 'in' testlist ':' 
		  suite 
		  ('else' ':' suite)? 
		  	-> ^(ST_FOR exprlist testlist suite suite?)
	;

try_stmt
    :   'try' ':' suite
        (   (except_clause ':' suite)+ ('else' ':' suite)?
        |   'finally' ':' suite
        )
	;

except_clause: 'except' (test (',' test)?)?
	;

suite: simple_stmt -> ^(STMT_LIST simple_stmt)
	| NEWLINE INDENT (stmt)+ DEDENT -> ^(STMT_LIST stmt+)
	;


test: and_test ('or'^ and_test)*
	| lambdef
	;

and_test
	: not_test ('and'^ not_test)*
	;

not_test
	: 'not'^ not_test
	| comparison
	;

comparison
@init {
    List<CommonTree> right = null;
    List<ComparisonOperator> operators = null;
}
@after {
  if ( operators != null )
  {
    $comparison.tree = new ComparisonTree( (CommonTree)left.tree, right, operators );
  }
}
    : left=expr 
        (c=comp_op r=expr  { if (operators==null) {
                                      operators = Lists.newArrayList();
                                      right = Lists.newArrayList();
                                   }
                                   operators.add( c.op );
                                   right.add( (CommonTree)$r.tree );  
                                 } )*
	;
	
comp_op returns [ComparisonOperator op]
  : LESS         { $op = ComparisonOperator.Less;         }
  | GREATER      { $op = ComparisonOperator.Greater;      }
  | EQUAL        { $op = ComparisonOperator.Equal;        }
  | GREATEREQUAL { $op = ComparisonOperator.GreaterEqual; }
  | LESSEQUAL    { $op = ComparisonOperator.LessEqual;    }
  | ALT_NOTEQUAL { $op = ComparisonOperator.NotEqual;     }
  | NOTEQUAL     { $op = ComparisonOperator.NotEqual;     }
  | 'in'         { $op = ComparisonOperator.In;           }
  | 'not' 'in'   { $op = ComparisonOperator.NotIn;        }
  | 'is'         { $op = ComparisonOperator.Is;           }
  | 'is' 'not'   { $op = ComparisonOperator.IsNot;        }
  ;

expr: xor_expr (VBAR^ xor_expr)*
	;

xor_expr: and_expr (CIRCUMFLEX^ and_expr)* ;
and_expr: shift_expr (AMPER^ shift_expr)* ;
arith_expr: term ((PLUS|MINUS)^ term)* ;
shift_expr: arith_expr (('<<'|'>>')^ arith_expr)* ;

term: factor (options {backtrack=true;}:(STAR | SLASH | PERCENT | DOUBLESLASH)^ factor)* ;

factor	
    : PLUS factor -> ^(UNARY_PLUS factor)
    | MINUS factor -> ^(UNARY_MINUS factor)
    | TILDE factor -> ^(TILDE factor)
	| power
	;

power :  atom (trailer^)*  
		(options {greedy=true;}:DOUBLESTAR^ factor)?
	;

atom: LPAREN! (testlist)? RPAREN!
	| LBRACK (listmaker)? RBRACK -> ^(LIST listmaker?)
	| LCURLY (dictorsetmaker -> dictorsetmaker
			   | -> ^(MAP) ) RCURLY 
	| BACKQUOTE testlist BACKQUOTE
	| NAME
	| INT
	| LONGINT
	| FLOAT
	| COMPLEX
	| (STRING)+
	| 'True' -> ST_TRUE
	| 'False' -> ST_FALSE
	| 'None' -> ST_NONE
	;

listmaker: test ( list_for | (options {greedy=true;}:COMMA! test)* ) (COMMA!)? 
	;

lambdef: 'lambda' (varargslist)? COLON test
		-> ^(ST_LAMBDA ^(PARAMETERS varargslist?) test)
	;

trailer: '(' (arglist-> ^(CALL ^(ARGS_LIST arglist)) | -> ^(CALL ARGS_LIST) ) ')' 
	| '[' subscriptlist ']' -> ^(SUBSCRIPT subscriptlist)
	| '.' NAME -> ^(GET_ATTR NAME)
	;

subscriptlist
    :   subscript (options {greedy=true;}:',' subscript)* (',')?
	;

subscript
	: '.' '.' '.'
    | test (':' (test)? (sliceop)?)?
    | ':' (test)? (sliceop)?
    ;

sliceop: ':' (test)?
	;

exprlist
    :   expr (options {k=2;}:',' expr)* (',')?
	;

testlist
    :   test (options {k=2;}: ',' test)* (',')? -> ^(TEST_LIST test+)
    ;
    
dictorsetmaker
    :   test ( 
    		':' test (options {k=2;}:',' test ':' test)*  -> ^(MAP test+)
    		| (options {k=2;}:',' test)* -> ^(SET test+)
    	)
    	(',')?
    ;
    
classdef: 'class' NAME (LPAREN testlist RPAREN)? COLON 
		  suite
		  -> ^(ST_CLASS NAME testlist? suite)
	;

arglist: positional_arguments
        ( ','! keyword_arguments )?
        ( ','!
            ( STAR test
                (','! DOUBLESTAR test)?
            | DOUBLESTAR test
        )?
        )? (COMMA!)?
    | keyword_arguments ( ','!
            ( STAR test
                (','! DOUBLESTAR test)?
            | DOUBLESTAR test
            )?
        )? (COMMA!)?
    |   STAR test (','! DOUBLESTAR test)? (COMMA!)?
    |   DOUBLESTAR test (COMMA!)?
    ;

positional_arguments: test (',' test)* -> test+;

keyword_arguments : keyword_item (',' keyword_item)*  -> ^(KEYWORDS keyword_item+ );
keyword_item : NAME '='! test ;

list_iter: list_for
	| list_if
	;

list_for: 'for' exprlist 'in' testlist (list_iter)?
	;

list_if: 'if' test (list_iter)?
	;

LPAREN	: '(' {implicitLineJoiningLevel++;} ;

RPAREN	: ')' {implicitLineJoiningLevel--;} ;

LBRACK	: '[' {implicitLineJoiningLevel++;} ;

RBRACK	: ']' {implicitLineJoiningLevel--;} ;

COLON 	: ':' ;

COMMA	: ',' ;

SEMI	: ';' ;

PLUS	: '+' ;

MINUS	: '-' ;

STAR	: '*' ;

SLASH	: '/' ;

VBAR	: '|' ;

AMPER	: '&' ;

LESS	: '<' ;

GREATER	: '>' ;

ASSIGN	: '=' ;

PERCENT	: '%' ;

BACKQUOTE	: '`' ;

LCURLY	: '{' {implicitLineJoiningLevel++;} ;

RCURLY	: '}' {implicitLineJoiningLevel--;} ;

CIRCUMFLEX	: '^' ;

TILDE	: '~' ;

EQUAL	: '==' ;

NOTEQUAL	: '!=' ;

ALT_NOTEQUAL: '<>' ;

LESSEQUAL	: '<=' ;

LEFTSHIFT	: '<<' ;

GREATEREQUAL	: '>=' ;

RIGHTSHIFT	: '>>' ;

PLUSEQUAL	: '+=' ;

MINUSEQUAL	: '-=' ;

DOUBLESTAR	: '**' ;

STAREQUAL	: '*=' ;

DOUBLESLASH	: '//' ;

SLASHEQUAL	: '/=' ;

VBAREQUAL	: '|=' ;

PERCENTEQUAL	: '%=' ;

AMPEREQUAL	: '&=' ;

CIRCUMFLEXEQUAL	: '^=' ;

LEFTSHIFTEQUAL	: '<<=' ;

RIGHTSHIFTEQUAL	: '>>=' ;

DOUBLESTAREQUAL	: '**=' ;

DOUBLESLASHEQUAL	: '//=' ;

DOT : '.' ;

FLOAT
	:	'.' DIGITS (Exponent)?
    |   DIGITS ('.' (DIGITS (Exponent)?)? | Exponent)
    ;

LONGINT
    :   INT ('l'|'L')
    ;

fragment
Exponent
	:	('e' | 'E') ( '+' | '-' )? DIGITS
	;

INT :   // Hex
        '0' ('x' | 'X') ( '0' .. '9' | 'a' .. 'f' | 'A' .. 'F' )+
        ('l' | 'L')?
    |   // Octal
        '0' DIGITS*
    |   '1'..'9' DIGITS*
    ;

COMPLEX
    :   INT ('j'|'J')
    |   FLOAT ('j'|'J')
    ;

fragment
DIGITS : ( '0' .. '9' )+ ;

NAME:	( 'a' .. 'z' | 'A' .. 'Z' | '_')
        ( 'a' .. 'z' | 'A' .. 'Z' | '_' | '0' .. '9' )*
    ;

/** Match various string types.  Note that greedy=false implies '''
 *  should make us exit loop not continue.
 */
STRING
    :   ('r'|'u'|'ur')?
        (   '\'\'\'' (options {greedy=false;}:.)* '\'\'\''
        |   '"""' (options {greedy=false;}:.)* '"""'
        |   '"' (ESC|~('\\'|'\n'|'"'))* '"'
        |   '\'' (ESC|~('\\'|'\n'|'\''))* '\''
        )
	;

fragment
ESC
	:	'\\' .
	;

/** Consume a newline and any whitespace at start of next line */
CONTINUED_LINE
	:	'\\' ('\r')? '\n' (' '|'\t')* { $channel=HIDDEN; }
	;

/** Treat a sequence of blank lines as a single blank line.  If
 *  nested within a (..), {..}, or [..], then ignore newlines.
 *  If the first newline starts in column one, they are to be ignored.
 */
NEWLINE
    :   (('\r')? '\n' )+
        {if ( startPos==0 || implicitLineJoiningLevel>0 )
            $channel=HIDDEN;
        }
    ;

WS	:	{startPos>0}?=> (' '|'\t')+ {$channel=HIDDEN;}
	;
	
/** Grab everything before a real symbol.  Then if newline, kill it
 *  as this is a blank line.  If whitespace followed by comment, kill it
 *  as it's a comment on a line by itself.
 *
 *  Ignore leading whitespace when nested in [..], (..), {..}.
 */
LEADING_WS
@init {
    int spaces = 0;
}
    :   {startPos==0}?=>
    	(   {implicitLineJoiningLevel>0}? ( ' ' | '\t' )+ {$channel=HIDDEN;}
       	|	( 	' '  { spaces++; }
        	|	'\t' { spaces += 8; spaces -= (spaces \% 8); }
       		)+
        	{
            // make a string of n spaces where n is column number - 1
            char[] indentation = new char[spaces];
            for (int i=0; i<spaces; i++) {
                indentation[i] = ' ';
            }
            String s = new String(indentation);
            emit(new ClassicToken(LEADING_WS,new String(indentation)));
        	}
        	// kill trailing newline if present and then ignore
        	( ('\r')? '\n' {if (state.token!=null) state.token.setChannel(HIDDEN); else $channel=HIDDEN;})*
        )
    ;

/** Comments not on line by themselves are turned into newlines.

    b = a # end of line comment

    or

    a = [1, # weird
         2]

    This rule is invoked directly by nextToken when the comment is in
    first column or when comment is on end of nonwhitespace line.

	Only match \n here if we didn't start on left edge; let NEWLINE return that.
	Kill if newlines if we live on a line by ourselves
	
	Consume any leading whitespace if it starts on left edge.
 */
COMMENT
@init {
    $channel=HIDDEN;
}
    :	{startPos==0}?=> (' '|'\t')* '#' (~'\n')* '\n'+
    |	{startPos>0}?=> '#' (~'\n')* // let NEWLINE handle \n unless char pos==0 for '#'
    ;
